Skip to content

zipper_algebra: further specialize grafting - #62

Merged
luketpeterson merged 3 commits into
masterfrom
algebra_grafts_optimization
Aug 21, 2026
Merged

zipper_algebra: further specialize grafting#62
luketpeterson merged 3 commits into
masterfrom
algebra_grafts_optimization

Conversation

@marcin-rzeznicki

Copy link
Copy Markdown
Collaborator

This leads to a few percent speed-up in the benchmarks. For instance, for symmetric difference simulation, before this we had:

ops               fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ xor                          │               │               │               │         │
├─ base_2b     84.04 ms      │ 85.36 ms      │ 84.28 ms      │ 84.47 ms      │ 5       │ 50
├─ base_4b     888.8 ms      │ 896.3 ms      │ 893.7 ms      │ 893 ms        │ 5       │ 50
├─ zipper2_2b  494.2 ms      │ 501 ms        │ 497.1 ms      │ 497.9 ms      │ 5       │ 50
├─ zipper2_4b  3.88 s        │ 3.897 s       │ 3.886 s       │ 3.887 s       │ 5       │ 50
├─ zipper4_2b  331.4 ms      │ 336.5 ms      │ 333 ms        │ 333.7 ms      │ 5       │ 50
╰─ zipper4_4b  2.4 s         │ 2.411 s       │ 2.403 s       │ 2.404 s       │ 5       │ 50

and now we have:

Timer precision: 33 ns
ops               fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ xor                          │               │               │               │         │
├─ base_2b     71.11 ms      │ 72.15 ms      │ 71.48 ms      │ 71.55 ms      │ 5       │ 50
├─ base_4b     838.6 ms      │ 858.7 ms      │ 840.8 ms      │ 844.1 ms      │ 5       │ 50
├─ zipper2_2b  308.1 ms      │ 312.8 ms      │ 310.3 ms      │ 310.2 ms      │ 5       │ 50
├─ zipper2_4b  2.625 s       │ 2.65 s        │ 2.635 s       │ 2.636 s       │ 5       │ 50
├─ zipper4_2b  158.1 ms      │ 159.3 ms      │ 158.7 ms      │ 158.7 ms      │ 5       │ 50
╰─ zipper4_4b  1.603 s       │ 1.617 s       │ 1.615 s       │ 1.612 s       │ 5       │ 50

so there is a quite considerable speed-up (especially for longer paths)

This leads to a few percent speed-up in the benchmarks
@adamv-symbolica

Copy link
Copy Markdown

Let's fold do-graft optimization into the masked version @marcin-rzeznicki

@marcin-rzeznicki

Copy link
Copy Markdown
Collaborator Author

@Adam-Vandervorst this is done in 353f4d4 @luketpeterson do you think this is the right place for the optimization?

Also, I tried to optimize it further by doing the following:

-        match child_mask.count_bits() {
+        let effective_mask = child_mask & src.child_mask();
+        match effective_mask.count_bits() {
             0 => {
                 if remove_unset {
                     self.remove_branches(false);
+                } else {
+                    self.remove_unmasked_branches(child_mask.not(), false);
                 }
             }
             1 => {
                 if remove_unset {
                     self.remove_branches(false);
+                } else {
+                    self.remove_unmasked_branches(child_mask.not(), false);
                 }
 
-                let byte = child_mask.indexed_bit::<true>(0).expect("one bit set");
+                let byte = effective_mask.indexed_bit::<true>(0).expect("one bit set");
                 self.descend_to_byte(byte);
                 self.graft_src_at(src, &[byte]);
                 self.ascend_byte();
             }
             2 => {
                 if remove_unset {
                     self.remove_branches(false);
+                } else {
+                    self.remove_unmasked_branches(child_mask.not(), false);
                 }
 
-                let first_byte = child_mask.indexed_bit::<true>(0).expect("some bit set");
+                let first_byte = effective_mask.indexed_bit::<true>(0).expect("some bit set");
                 self.descend_to_byte(first_byte);
                 self.graft_src_at(src, &[first_byte]);
                 self.ascend_byte();
 
-                let second_byte = child_mask.next_bit(first_byte).expect("two bits set");
+                let second_byte = effective_mask.next_bit(first_byte).expect("two bits set");

But this fails miserably in some of the tests by triggering the assertion in remove_unmasked_branches:

thread 'experimental::zipper_algebra::tests::join::test_deep_shared_prefix_then_split_n' (65048) panicked at src/dense_byte_node.rs:1124:9:
assertion failed: key.len() == 0

@luketpeterson why would the assertion fail?

Fixing issue with remove_unmasked_branches at non-existent paths, and adding test
@luketpeterson

Copy link
Copy Markdown
Collaborator

why would the assertion fail?

Your code exposed a bug affecting non-existent paths. I pushed a fix.

Also, I tried to optimize it further by doing the following:

Unfortunately, that approach is slower.

To focus on the case where it would matter, I created a synthetic benchmark where I was passing a variable child_mask, but grafting from a source with 0, 1, or 2 downstream branches. So the effective mask would go down one of those cases. Here are the results on my laptop.

   Mask bits    Branch head    Modified approach    Change
  ━━━━━━━━━━━  ━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━  ━━━━━━━━
           3          96 ns             182 ns      +90%
  ───────────  ─────────────  ─────────────────  ────────
           4         102 ns             177 ns      +74%
  ───────────  ─────────────  ─────────────────  ────────
           6         114 ns             187 ns      +64%
  ───────────  ─────────────  ─────────────────  ────────
           8         130 ns             193 ns      +49%
  ───────────  ─────────────  ─────────────────  ────────
          15         181 ns             208 ns      +15%
  ───────────  ─────────────  ─────────────────  ────────
          30         271 ns             244 ns      -10%
  ───────────  ─────────────  ─────────────────  ────────
          60         402 ns             333 ns      -17%
  ───────────  ─────────────  ─────────────────  ────────
         100         583 ns             448 ns      -23%

So it does pull ahead when graft_masked_branches is being used as a glorified remove. But using it this way is an anti-pattern. It works, but it's not what the API was designed for. So sacrificing performance in the intended use case to improve an improper use case is not a win. If the user knows they are masking out far more branches than they intend to graft, they can do the remove_unmasked_branches themselves and pass a more reasonable child mask.

@luketpeterson luketpeterson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already wrote some comments / made the tweaks I had in mind.

@luketpeterson
luketpeterson merged commit 1c7f889 into master Aug 21, 2026
@marcin-rzeznicki

Copy link
Copy Markdown
Collaborator Author

Thank you, @luketpeterson! I agree with your reasoning 100%. I'll push the relevant code (that keeps the masks reasonable by intersecting them) to the zipper_algebra module based on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants